﻿
namespace HSS.ComponentModel
{
	#region Using Directives
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Reflection;
	using System.Security;
	using System.Windows;
	using System.Windows.Media;
	#endregion

	#region HSSPropertyDescriptor
	/// <summary>
	/// HSSPropertyDescriptor hold a reference to an individual property in the propertygrid
	/// </summary>
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "HSS")]
	public sealed class HSSPropertyDescriptor : NotifiableObject
	{
		#region Events

		#region ValueError
		/// <summary>
		/// Occurs when an error is encountered attempting to set the Value
		/// </summary>
		public event EventHandler<ExceptionEventArgs> ValueError;
		/// <summary>
		/// Raises the ValueError event
		/// </summary>
		/// <param name="ex">The exception</param>
		private void OnValueError(Exception ex)
		{
			var handler = ValueError;
			if (null != handler)
				handler(this, new ExceptionEventArgs(ex));
		}
		#endregion

		#region ValueChanged
		/// <summary>
		/// Event that is raised when the property value changes.
		/// </summary>
		public event EventHandler<EventArgs> ValueChanged;
		private void OnValueChanged()
		{
			var handler = this.ValueChanged;
			if (null != handler)
				handler(this, EventArgs.Empty);
		}
		#endregion

		#endregion

		#region Fields
		//private static Type stringType = typeof(string);
		private static Dictionary<string, string> editors;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="property">The <see cref="PropertyInfo"/> to be described.</param>
		public HSSPropertyDescriptor(PropertyInfo property)
		{
			Guard.ArgumentNotNull(property, "property");

			this.Property = property;
			var attr = GetAttribute<ReadOnlyAttribute>();
			if (attr != null)
				this.IsReadOnly = attr.IsReadOnly;
			if (property.PropertyType == typeof(Type))
			{
				this.IsReadOnly = true;
				return;
			}
			if (!this.IsReadOnly)
			{
				if (property.CanWrite)
				{
					var setMethod = property.GetSetMethod();
					if (null != setMethod)
					{
						var s = TypeDescriptor.GetAttribute<SecurityCriticalAttribute>(setMethod);
						if (null != s)
							this.IsReadOnly = true;
					}
					else
					{
						if (!typeof(IList).IsAssignableFrom(property.PropertyType))
							this.IsReadOnly = true;
					}
				}
			}
		}
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="property">The <see cref="PropertyInfo"/> to be described.</param>
		/// <param name="category">The desired Category.</param>
		public HSSPropertyDescriptor(PropertyInfo property, string category)
			: this(property)
		{
			_category = category;
		}
		/// <summary>
		/// CCTOR
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
		static HSSPropertyDescriptor()
		{
			editors = new Dictionary<string, string>();
			editors.Add(typeof(DateTime).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.DateTimeValueEditor));
			editors.Add(typeof(Boolean).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.BooleanValueEditor));
			editors.Add(typeof(Enum).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.EnumValueEditor));
			editors.Add(typeof(String).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.StringValueEditor));
			editors.Add(typeof(Color).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.ColorValueEditor));
			editors.Add(typeof(ValueType).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.StringValueEditor));
			editors.Add(typeof(IList).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.CollectionValueEditor));
			editors.Add(typeof(Object).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.ObjectValueEditor));
			editors.Add(typeof(Thickness).FullName, BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.ThicknessValueEditor));
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets the Name of the current Property
		/// </summary>
		public string Name
		{
			get { return this.Property.Name; }
		}
		/// <summary>
		/// Gets the optional Display Name
		/// </summary>
		public string DisplayName
		{
			get
			{
				if (string.IsNullOrEmpty(_displayName))
				{
					DisplayNameAttribute attr = GetAttribute<DisplayNameAttribute>();
					_displayName = (attr != null) ? attr.DisplayName : this.Name;
				}

				return _displayName;
			}
		} private string _displayName;
		/// <summary>
		/// Gets the optional Description
		/// </summary>
		public string Description
		{
			get
			{
				if (string.IsNullOrEmpty(_description))
				{
					DescriptionAttribute attr = this.GetAttribute<DescriptionAttribute>();
					_description = (attr != null) ? attr.Description : this.DisplayName;
				}

				return _description;
			}
		} private string _description;
		/// <summary>
		/// Gets the optional Category
		/// </summary>
		public string Category
		{
			get
			{
				if (string.IsNullOrEmpty(_category))
				{
					CategoryAttribute attr = GetAttribute<CategoryAttribute>();
					if (attr != null && !string.IsNullOrEmpty(attr.Category))
						_category = attr.Category;
					else
						_category = "Misc";
				}
				return this._category;
			}
		} private string _category;
		/// <summary>
		/// Gets the default value if the property contains a <see cref="DefaultValueAttribute"/>
		/// </summary>
		public object DefaultValue
		{
			get
			{
				if (defaultValue == Type.Missing)
				{
					var attribute = this.GetAttribute<DefaultValueAttribute>();
					this.defaultValue = null;
					if (attribute != null)
						this.defaultValue = attribute.Value;
				}
				return this.defaultValue;
			}
		} object defaultValue = Type.Missing;
		/// <summary>
		/// Gets the Type of the Property
		/// </summary>
		public Type PropertyType
		{
			get { return this.Property.PropertyType; }
		}
		/// <summary>
		/// Gets if the value can be written to
		/// </summary>
		public bool CanWrite
		{
			get { return this.Property.CanWrite; }
		}
		/// <summary>
		/// Gets if the Property is ReadOnly
		/// </summary>
		public bool IsReadOnly
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets the PropertyInfo for this descriptor
		/// </summary>
		public PropertyInfo Property { get; private set; }
		/// <summary>
		/// Gets the TypeConverter for this descriptor.
		/// </summary>
		public TypeConverter Converter
		{
			get
			{
				if (null == _typeConverter)
					_typeConverter = TypeDescriptor.GetConverter(this.Property);
				return _typeConverter;
			}
		} TypeConverter _typeConverter;
		/// <summary>
		/// Gets a DisplayNameComparer
		/// </summary>
		internal static DisplayNameComparer Comparer
		{
			get { return new DisplayNameComparer(); }
		}
		/// <summary>
		/// Gets if the property type is an Enum and has the <see cref="FlagsAttribute"/>.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Flags")]
		public bool HasFlags
		{
			get
			{
				if (!_hasFlags.HasValue)
				{
					_hasFlags = false;
					if (this.PropertyType.IsEnum)
					{
						FlagsAttribute flags = TypeDescriptor.GetAttribute<FlagsAttribute>(this.PropertyType);
						if (null != flags)
							_hasFlags = true;
					}
				}
				return _hasFlags.Value;
			}
		} bool? _hasFlags = default(bool?);
		#endregion

		#region Methods

		/// <summary>
		/// Get the specified attribute
		/// </summary>
		/// <typeparam name="T">The Type of attribute to retrieve</typeparam>
		/// <returns>The request attribute</returns>
		public T GetAttribute<T>() where T : Attribute
		{
			return TypeDescriptor.GetAttribute<T>(this.Property);
		}
		/// <summary>
		/// Creates and returns the Editor for this Property
		/// </summary>
		/// <returns>An Property Editor</returns>
		public object GetEditor()
		{
			object editor = null;

			// Get from Attribute if exists			
			var attribute = this.GetAttribute<EditorAttribute>();
			if (attribute != null)
			{
				var editorType = Type.GetType(attribute.EditorTypeName, false);
				if (null != editorType)
					editor = CreateEditor(editorType.AssemblyQualifiedName, this);
			}

			// Get from list of predefined editors
			if (null == editor)
				editor = GetEditor(this.PropertyType, this);

			return editor;
		}

		/// <summary>
		/// Gets the value from the supplied instance.
		/// </summary>
		/// <param name="instance">The instance that contains the property</param>
		/// <returns>The instance value</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public object GetValue(object instance)
		{
			try
			{
				return this.Property.GetValue(instance, null);
			}
			catch (SecurityException sex)
			{
				// Some places in SL does not allow Reading a value
				// if not in elevated-trust mode.
				Debug.WriteLine(sex.ToString());
			}
			catch (TargetInvocationException tex)
			{
				this.IsReadOnly = true;
				if (null != tex.InnerException)
				{
					if (tex.InnerException is ArgumentNullException)
					{
						// Some places in SL, there is a defect
						// where it cannot locate an Environment
						// resource string and throws an 
						// ArgumentNullException, which is wrapped
						// by reflection into an TargetInvocationException
						Debug.WriteLine(tex.InnerException.ToString());
					}
					else if (tex.InnerException is SecurityException)
					{
						// Some places in SL does not allow Reading a value
						// if not in elevated-trust mode.
						Debug.WriteLine(tex.InnerException.ToString());
					}
					else if (tex.InnerException is InvalidOperationException)
					{
						// Some objects does not allow Reading a value.
						Debug.WriteLine(tex.InnerException.ToString());
					}
					//else
					//    LogService.WriteException(tex.InnerException);
				}
				else
				{
					this.IsReadOnly = true;
					//LogService.WriteException(tex);
				}
			}
			catch (MethodAccessException)
			{
				this.IsReadOnly = true;
			}
			catch (ArgumentNullException anex)
			{
				// Some places in SL, there is a defect
				// where it cannot locate an Environment
				// resource string
				Debug.WriteLine(anex.ToString());
				this.IsReadOnly = true;
			}
			catch (Exception)
			{
				this.IsReadOnly = true;
				//LogService.WriteException(ex);
			}

			return null;
		}
		/// <summary>
		/// Set the value on the supplied instance
		/// </summary>
		/// <param name="instance">The instance object that contains the property</param>
		/// <param name="value">The value to set</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public void SetValue(object instance, object value)
		{
			if (this.IsReadOnly)
				return;
			if (this.Property.GetSetMethod() == null)
				return;
			try
			{
				var propertyType = this.PropertyType;
				if (((propertyType == typeof(object)) || ((value == null) && propertyType.IsClass)) || ((value != null) && propertyType.IsAssignableFrom(value.GetType())))
				{
					this.Property.SetValue(instance, value, (BindingFlags.NonPublic | BindingFlags.Public), null, null, null);
					this.OnPropertyChanged("Value");
					this.OnValueChanged();
				}
				else
				{
					try
					{
						if (propertyType.IsEnum)
						{
							if (this.HasFlags)
							{
								var tc = this.Converter;
								if (tc != null)
								{
									object convertedValue = tc.ConvertFrom(value);
									this.Property.SetValue(instance, convertedValue, null);
									this.OnPropertyChanged("Value");
									this.OnValueChanged();
								}
							}
							else
							{
								object val = Enum.Parse(this.PropertyType, value.ToString(), false);
								this.Property.SetValue(instance, val, (BindingFlags.NonPublic | BindingFlags.Public), null, null, null);
								this.OnPropertyChanged("Value");
								this.OnValueChanged();
							}
						}
						else
						{
							var tc = this.Converter;
							if (tc != null)
							{
								object convertedValue = tc.ConvertFrom(value);
								this.Property.SetValue(instance, convertedValue, null);
								this.OnPropertyChanged("Value");
								this.OnValueChanged();
							}
							else
							{
								// try direct setting as a string...
								this.Property.SetValue(instance, value.ToString(), (BindingFlags.NonPublic | BindingFlags.Public), null, null, null);
								this.OnPropertyChanged("Value");
								this.OnValueChanged();
							}
						}
					}
					catch (Exception ex)
					{
						this.OnPropertyChanged("Value");
						this.OnValueChanged();
						this.OnValueError(ex);
					}
				}
			}
			catch (MethodAccessException mex)
			{
				this.IsReadOnly = true;
				this.OnPropertyChanged("Value");
				this.OnValueChanged();
				this.OnPropertyChanged("CanWrite");
				this.OnValueError(mex);
			}
		}

		/// <summary>
		/// Forces the Descriptor's IsReadOnly property.
		/// </summary>
		internal void ForceSetReadOnly(bool isReadOnly)
		{
			this.IsReadOnly = isReadOnly;
		}

		private static object GetEditor(Type propertyType, HSSPropertyDescriptor descriptor)
		{
			object editor = null;

			// Get default editor
			editor = GetEditor(propertyType.FullName, descriptor);

			// Get default IList editor (CollectionValueEditor)
			if (null == editor && typeof(IList).IsAssignableFrom(propertyType))
				editor = GetEditor(typeof(IList).FullName, descriptor);

			// Get default Enum editor (EnumValueEditor)
			if (null == editor && propertyType.IsEnum)
				editor = GetEditor(typeof(Enum).FullName, descriptor);

			// Try from BaseType
			if (null == editor)
			{
				var baseType = propertyType;
				while (editor == null && baseType.BaseType != null)
				{
					baseType = baseType.BaseType;
					if (!baseType.IsAbstract && baseType.IsPublic && baseType != typeof(object))
						editor = GetEditor(baseType.FullName, descriptor);
				}
			}

			// Get default ValueType editor (string editor)
			if (null == editor && propertyType.IsValueType)
				editor = GetEditor(typeof(ValueType).FullName, descriptor);

			// Try object editor
			if (null == editor)
				editor = GetEditor(typeof(Object).FullName, descriptor);

			return editor;
		}
		private static object GetEditor(string typeFullName, HSSPropertyDescriptor descriptor)
		{
			object editor = null;
			string editorAQN;

			if (!editors.TryGetValue(typeFullName, out editorAQN))
				return editor;

			return CreateEditor(editorAQN, descriptor);
		}
		private static object CreateEditor(string editorAQN, HSSPropertyDescriptor descriptor)
		{
			object editor = null;

			var editorType = Type.GetType(editorAQN, false);
			if (null != editorType)
			{
				// If the propertyType is an Enum only, then go ahead, but if it
				// is also a Flag, than use the EnumFlagValueEditor. 
				if (descriptor.HasFlags)
					editor = Activator.CreateInstance(Type.GetType(BuiltInEditor.GetAssemblyQualifiedName(BuiltInEditors.EnumFlagValueEditor), false), descriptor);
				else
					editor = Activator.CreateInstance(editorType, descriptor);
			}

			return editor;
		}

		#endregion
	}
	#endregion

	#region IComparer
	/// <summary>
	/// Compares two HSSPropertyDescriptor based on their DisplayName
	/// </summary>
	internal sealed class DisplayNameComparer : IComparer<HSSPropertyDescriptor>
	{
		#region IComparer<HSSPropertyDescriptor> Members
		/// <summary>
		/// Compare two HSSPropertyDescriptor
		/// </summary>
		/// <param name="x">HSSPropertyDescriptor</param>
		/// <param name="y">HSSPropertyDescriptor</param>
		/// <returns>Are the instance the same based on the DisplayName</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public int Compare(HSSPropertyDescriptor x, HSSPropertyDescriptor y)
		{
			var xv = x as object;
			var yv = y as object;
			if (xv == null && yv == null)
				return 0;
			if (xv == null && yv != null)
				return 1;
			if (xv != null && yv == null)
				return -1;

			return string.Compare(x.DisplayName, y.DisplayName, StringComparison.Ordinal);
		}

		#endregion
	}
	#endregion
}
